package runner

import (
	"bufio"
	"ckeadm/util"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	ckeadmapi "ckeadm/cmd/apis"
	"ckeadm/constants"
	"github.com/fatih/color"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

type GetInstallMsgsFunction func(name, ip, clusterId, doneFile string) (logs []string)

/* about this function: =>
   first, get information from GetInstallMsgs
   second, get running status from ckeadm running container
   thrid, transform information into a map
   lastest, show it in giving writer

about parameters: =>
  ctx: data of configration
  writer: logs for write
  doneFile: done file name. up.done, scale.done, scale.down.done, upgrade.done
  GetInstallMsgs: function for get doneContext
*/
func CkeadmLogsRunner(ctx *cli.Context, writer io.Writer, doneFile string, GetInstallMsgs GetInstallMsgsFunction) error {
	config, err := GetConfigFromFile(ctx.String("f"))
	if err != nil {
		return fmt.Errorf("error while get config from %s, err: %s", ctx.String("f"), err.Error())
	}
	logrus.SetLevel(logrus.Level(ctx.Int("v")))

	if len(config.Nodes) == 0 {
		fmt.Fprintln(writer, "No resources found in this configration.")
		return nil
	}

	var progress = make(map[string]ckeadmapi.InstallInfo, 0)
	var nodeInfo = make(map[string][]string, 0)
	var mux = sync.Mutex{}
	progress["NAME"] = ckeadmapi.InstallInfo{Name: "NAME", Status: "STATUS", Progress: "PROGRESS", Message: "MESSAGE"}

	wg := sync.WaitGroup{}
	wg.Add(len(config.Nodes))

	// get results from diff nodes, to check the result file.
	for _, node := range config.Nodes {
		go func(node ckeadmapi.CkeConfigNode) {
			defer wg.Done()

			mux.Lock() // lock, make sure not concurrent write
			defer mux.Unlock()

			nodeInfo[node.Name] = GetInstallMsgs(node.Name, node.Address, config.ClusterName, doneFile)
		}(node)
	}

	// get results from main progress, in the gensiss node.
	var MainProcessStatus, RunningTime, MainProcessLogs string
	var MainProgress int
	// #nosec
	out, err := exec.Command("docker", "inspect", constants.CkeDeployContainerName+"-"+config.ClusterName, "-f", "{{.State.Status}}${{.Created}}${{.State.FinishedAt}}${{.LogPath}}").CombinedOutput()
	if err == nil && strings.Count(string(out), "$") == 3 {
		MainProcessStatus = strings.Split(string(out), "$")[0]
		CreateTime, _ := time.ParseInLocation("2006-01-02T15:04:05", strings.Split(string(out), "$")[1][:19], time.UTC)
		FinishTime, _ := time.ParseInLocation("2006-01-02T15:04:05", strings.Split(string(out), "$")[2][:19], time.UTC)

		if logs, err := ioutil.ReadFile(strings.ReplaceAll(strings.Split(string(out), "$")[3], "\n", "")); err == nil {
			MainProcessLogs = string(logs)
		}

		if FinishTime.Before(CreateTime) {
			RunningTime = time.Now().Sub(CreateTime).String()
		} else {
			RunningTime = FinishTime.Sub(CreateTime).String()
		}
	} else {
		MainProcessStatus = "exited"
		RunningTime = "0s"
	}

	if constants.Debug {
		DockerCommand := fmt.Sprintln("docker", "inspect", constants.CkeDeployContainerName+"-"+config.ClusterName, "-f", "{{.State.Status}}${{.Created}}${{.State.FinishedAt}}${{.LogPath}}")
		logrus.Warningln("docker command:", DockerCommand)
		logrus.Warningln("get result:", out, err)
	}
	wg.Wait()

	for _, node := range config.Nodes {
		func(node ckeadmapi.CkeConfigNode) {
			progress[node.Name] = GetNodeInstallAllInfo(constants.CkectlUpPhaseNum+1, node, nodeInfo[node.Name], MainProcessStatus == "running", MainProcessLogs)
			pro, _ := strconv.Atoi(strings.ReplaceAll(progress[node.Name].Progress, "%", ""))
			MainProgress += pro
		}(node)
	}

	fmt.Fprintf(writer, "status:   ")
	if MainProcessStatus == "running" { // if running, show with color green
		color.Set(color.FgGreen, color.Bold)
		fmt.Fprintf(writer, "%s\n", MainProcessStatus)
		color.Unset()
	} else {
		fmt.Fprintf(writer, "%s\n", MainProcessStatus)
	}

	fmt.Fprintf(writer, "progress: %d%%\n", MainProgress/len(config.Nodes))
	fmt.Fprintf(writer, "time:     %s\n\n", RunningTime)
	ShowWatchNodesResult(config.Nodes, progress, writer)
	return nil
}

func ShowWatchNodesResult(nodes []ckeadmapi.CkeConfigNode, info map[string]ckeadmapi.InstallInfo, writer io.Writer) {
	const LenMax = 150
	var name_max, status_max, progress_max, mess_max int
	for _, node := range info {
		if name_max < len(node.Name) && len(node.Name) < LenMax {
			name_max = len(node.Name)
		}
		if status_max < len(node.Status) && len(node.Status) < LenMax {
			status_max = len(node.Status)
		}
		if progress_max < len(node.Progress) && len(node.Progress) < LenMax {
			progress_max = len(node.Progress)
		}
		if mess_max < len(node.Message) {
			mess_max = len(node.Message)
		}
	}

	fmt.Fprintf(writer, "%s   %s   %s   %s\n",
		ToLength(info["NAME"].Name, name_max),
		ToLength(info["NAME"].Status, status_max),
		ToLength(info["NAME"].Progress, progress_max),
		ToLength(info["NAME"].Message, mess_max),
	)
	for _, node := range nodes {
		fmt.Fprintf(writer, "%s   %s   %s   %s\n",
			ToLength(info[node.Name].Name, name_max),
			ToLength(info[node.Name].Status, status_max),
			ToLength(info[node.Name].Progress, progress_max),
			ToLength(info[node.Name].Message, mess_max),
		)
	}
}

func ToLength(str string, le int) string {
	if len(str) >= le {
		return str[:le]
	}

	for i := 0; len(str) < le; i++ {
		str += " "
	}

	return str
}

// status in ["Running", "Completed", "Failed", "Stopped"]
func GetNodeInstallAllInfo(num int, node ckeadmapi.CkeConfigNode, nodeMesgs []string, mainProcessRunning bool, mainProcessLogs string) ckeadmapi.InstallInfo {
	var ckerunning = mainProcessRunning
	var msg = nodeMesgs

	if regexp.MustCompile(fmt.Sprintf(`[\s\S]*\[%s\].*{.*\\"cmd\\": \\"%s [\s\S]*`, node.Name, constants.WhichCkectl)).MatchString(mainProcessLogs) {
		ckerunning = false
	}

	if constants.Debug {
		var MatchString = fmt.Sprintf(`[\s\S]*\[%s\].*{.*\\"cmd\\": \\"%s [\s\S]*`, node.Name, constants.WhichCkectl)
		var Logs = mainProcessLogs

		logrus.Warningln("MatchString is:", MatchString)
		logrus.Warningln("Logs is:", Logs)
		logrus.Warningln(regexp.MustCompile(MatchString).MatchString(Logs))
	}

	var status, mss string
	if ckerunning {
		status = "Running"
	}

	if len(msg) != 0 && msg[len(msg)-1] == "Done" {
		if !ckerunning {
			status = "Completed"
		}
		return ckeadmapi.InstallInfo{
			Name:     node.Name,
			Status:   status,
			Progress: "100%",
			Message:  "Done",
		}
	}

	if !ckerunning {
		if len(msg) != 0 && strings.Contains(msg[len(msg)-1], " Fail ") {
			status = "Failed"
		} else {
			status = "Stopped"
		}
	}

	// if mss is null, set it to start to install.
	if len(msg) == 0 {
		mss = "prepare to install node ..."
	} else {
		mss = msg[len(msg)-1]
	}

	return ckeadmapi.InstallInfo{
		Name:     node.Name,
		Status:   status,
		Progress: strconv.Itoa(100*len(msg)/num) + "%",
		Message:  mss,
	}
}

func FilterSpace(oldSls []string) (newSls []string) {
	for _, word := range oldSls {
		if word != "" {
			newSls = append(newSls, word)
		}
	}
	return newSls
}

func ShowInstallStatus(filePath string) error {
	files, err := util.ListAllFilesInPath(filePath)
	if err != nil {
		logrus.Errorf("List ckeadm wait log files failed: %s, maybe no cluster is installing.", err.Error())
		return err
	}
	totalTime := ""
	beginTime := time.Time{}
	endTime := time.Time{}
	var statusInfoList = make([]ckeadmapi.InstallStatusInfo, 0)

	for _, file := range files {
		reader, err := os.Open(filepath.Clean(file))
		if err != nil {
			logrus.Errorf("Read ckeadm wait log file failed: %s", err.Error())
			return err
		}
		rd := bufio.NewReader(reader)
		for {
			line, err := rd.ReadString('\n')
			if err != nil || io.EOF == err {
				break
			}
			if strings.Contains(line, "#") {
				statusInfo := ckeadmapi.InstallStatusInfo{}
				statusArray := strings.Split(line, "#")
				if len(statusArray) >= 4 {
					statusInfo.Name = statusArray[0]
					statusInfo.Status = statusArray[1]
					statusInfo.Progress = statusArray[2]
					statusInfo.Message = strings.ReplaceAll(statusArray[3], "\n", "")
					statusInfoList = append(statusInfoList, statusInfo)
				}
			}
			if strings.Contains(line, "BeginTime") {
				beginTime, err = time.ParseInLocation(time.RFC3339, strings.ReplaceAll(strings.Split(line, "#")[1], "\n", ""), time.Local)
				if err != nil {
					logrus.Errorf("Parse BeginTime failed: %s", err.Error())
					totalTime = constants.CkeStatusUnknown
				}
				//logrus.Infof("beginTime %s", beginTime.String())
			}
			if strings.Contains(line, "EndTime") {
				endTime, err = time.ParseInLocation(time.RFC3339, strings.ReplaceAll(strings.Split(line, "#")[1], "\n", ""), time.Local)
				if err != nil {
					logrus.Errorf("Parse EndTime failed: %s", err.Error())
					totalTime = constants.CkeStatusUnknown
				}
				//logrus.Infof("endTime %s", endTime.String())
			}
		}

		_ = reader.Close()
	}

	if totalTime != constants.CkeStatusUnknown {
		if !endTime.IsZero() {
			totalTime = endTime.Sub(beginTime).String()
		} else {
			totalTime = time.Now().Sub(beginTime).String()
		}
	}

	progressSum := 0
	totalStatus := constants.CkeStatusExited
	for _, item := range statusInfoList {
		if item.Status == constants.CkeStatusPrepare {
			totalStatus = constants.CkeStatusPrepare
		}
		if item.Status == constants.CkeStatusInstall {
			totalStatus = constants.CkeStatusInstall
		}
		percent, err := strconv.Atoi(strings.ReplaceAll(item.Progress, "%", ""))
		if err != nil {
			logrus.Errorf("Parse progress failed: %s", err.Error())
			percent = 0
		}
		progressSum += percent
	}
	totalProgress := "0%"
	if len(statusInfoList) != 0 {
		totalProgress = strconv.Itoa(progressSum/len(statusInfoList)) + "%"
	}

	//sort
	sort.Sort(util.StatusInfoSort(statusInfoList))

	//display
	statusList := []ckeadmapi.InstallStatusInfo{ckeadmapi.InstallStatusInfo{"NAME", "STATUS", "PROGRESS", "MESSAGE"}}
	statusList = append(statusList, statusInfoList...)
	ShowStatusList(totalStatus, totalProgress, totalTime, statusList, os.Stdout)
	return nil
}

func ShowStatusList(totalStatus, totalProgress, totalTime string, statusInfoList []ckeadmapi.InstallStatusInfo, writer io.Writer) {
	const LenMax = 150
	//totalStatus
	_, _ = fmt.Fprintf(writer, "%s   %s\n%s   %s\n%s   %s\n\n",
		ToLength("status:", 10), ToLength(totalStatus, 15),
		ToLength("progress:", 10), ToLength(totalProgress, 15),
		ToLength("time:", 10), ToLength(totalTime, 15),
	)

	var nameMax, statusMax, progressMax, messageMax int
	for _, row := range statusInfoList {
		if nameMax < len(row.Name) && len(row.Name) < LenMax {
			nameMax = len(row.Name)
		}
		if statusMax < len(row.Status) && len(row.Status) < LenMax {
			statusMax = len(row.Status)
		}
		if progressMax < len(row.Progress) && len(row.Progress) < LenMax {
			progressMax = len(row.Progress)
		}
		if messageMax < len(row.Message) && len(row.Message) < LenMax {
			messageMax = len(row.Message)
		}
	}

	for _, row := range statusInfoList {
		_, _ = fmt.Fprintf(writer, "%s   %s   %s   %s\n",
			ToLength(row.Name, nameMax),
			ToLength(row.Status, statusMax),
			ToLength(row.Progress, progressMax),
			ToLength(row.Message, messageMax),
		)
	}
}
